import os.path as osp
import time
import random
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
from tools import mkdir_if_missing, write_json

def try_close_login_tip(driver):
    """
    自动关闭 B站登录弹窗，如果有的话
    """
    try:
        close_btn = driver.find_element(By.CSS_SELECTOR, '.login-tip .close')
        if close_btn.is_displayed():
            close_btn.click()
            print("检测到登录弹窗，已自动关闭。")
            time.sleep(0.5)
    except Exception:
        pass  # 没有弹窗时什么都不做

class Bilibili_Spider():
    def __init__(
        self,
        uid,
        executable_path=r'D:\chrome\depot_tools\chromium\src\out\Default\chromedriver.exe',
        save_dir_json='json',
        t=2
    ):
        self.t = t
        self.uid = uid.strip()
        self.user_url = f'https://space.bilibili.com/{self.uid}/video?tid=0&keyword=&order=pubdate'
        self.save_dir_json = save_dir_json
        self.executable_path = executable_path
        options = ChromeOptions()
        options.binary_location = r'D:\chrome\depot_tools\chromium\src\out\Default\chrome.exe'
        options.add_argument("--disable-blink-features=AutomationControlled")
        service = Service(self.executable_path)
        self.browser = webdriver.Chrome(service=service, options=options)
        print('spider init done.')

    def close(self):
        self.browser.quit()

    def wait_and_scroll_to_bottom(self, max_round=10):
        """
        滚动到页面底部，触发更多内容加载
        """
        last_height = self.browser.execute_script("return document.body.scrollHeight")
        for _ in range(max_round):
            self.browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            time.sleep(0.7 + random.random())
            try_close_login_tip(self.browser)
            new_height = self.browser.execute_script("return document.body.scrollHeight")
            if new_height == last_height:
                break
            last_height = new_height

    def get_user_info(self):
        """
        返回 用户名 以及总视频数
        """
        self.browser.get(self.user_url)
        # 等所有视频卡片至少1个出现（B站页面结构慢）
        WebDriverWait(self.browser, 15).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".upload-video-card.grid-mode"))
        )
        time.sleep(self.t + random.random())
        html = BeautifulSoup(self.browser.page_source, features="html.parser")
        nickname_div = html.select_one('div.nickname')
        user_name = nickname_div.get_text(strip=True) if nickname_div else f"user_{self.uid}"
        total_count_span = html.find('span', class_='side-nav__item__sub-text')
        total_count = int(total_count_span.get_text(strip=True)) if total_count_span else 0
        return user_name, total_count

    def parse_videos_in_page(self):
        """
        返回该页所有视频卡片的基本信息（返回bv列表、url、title，页面没内容就返回空list）
        """
        import re
        html = BeautifulSoup(self.browser.page_source, features="html.parser")
        video_cards = html.find_all('div', class_='upload-video-card grid-mode')
        urls, titles, bvs = [], [], []
        for card in video_cards:
            title_a = card.select_one('.bili-video-card__title a')
            if title_a:
                title = title_a.get_text(strip=True)
                url = title_a.get('href')
                if url and url.startswith('//'):
                    url = 'https:' + url
                url_plain = url.split('?')[0] if url else ""
                if url_plain and not url_plain.endswith('/'):
                    url_plain += '/'
                bv_match = re.search(r'/video/([^/?]+)/', url_plain or "")
                bv = bv_match.group(1) if bv_match else ''
                if bv:
                    urls.append(url_plain)
                    titles.append(title)
                    bvs.append(bv)
        return urls, titles, bvs

    def get(self):
        print('Start ...\n')
        user_name, total_count = self.get_user_info()
        print(f'User Name: {user_name}, Total Videos: {total_count}')

        bvs_set = set()
        data_list = []
        page_num = 0

        while True:
            page_num += 1
            print(f'>>>> 正在采集第 {page_num} 页...')
            self.wait_and_scroll_to_bottom()
            urls, titles, bvs = self.parse_videos_in_page()
            cnt_new = 0
            for u, t, bv in zip(urls, titles, bvs):
                if bv not in bvs_set:
                    bvs_set.add(bv)
                    data_list.append({
                        'user_name': user_name,
                        'bv': bv,
                        'url': u,
                        'title': t
                    })
                    cnt_new += 1
            print(f'本页采集到新视频 {cnt_new} 个，累计 {len(data_list)} 个。\n')

            # 准备下一页
            try:
                self.browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
                time.sleep(0.5)
                try_close_login_tip(self.browser)
                next_btns = self.browser.find_elements(
                    By.XPATH,
                    '//button[contains(@class,"vui_pagenation--btn") and contains(text(),"下一页")]'
                )
                if not next_btns:
                    print('未检测到“下一页”按钮，采集结束。')
                    break
                next_btn = next_btns[0]
                if not next_btn.is_displayed() or not next_btn.is_enabled():
                    print('“下一页”按钮已隐藏或不可用，采集结束。')
                    break
                self.browser.execute_script("arguments[0].scrollIntoView(true);", next_btn)
                time.sleep(0.3)
                try_close_login_tip(self.browser)
                next_btn.click()
                # 等待新视频区渲染完成
                WebDriverWait(self.browser, 15).until(
                    EC.presence_of_element_located((By.CSS_SELECTOR, ".upload-video-card.grid-mode"))
                )
                time.sleep(self.t + random.random())
            except Exception as e:
                print(f'MSG: 下一页点击失败或已经是最后一页，异常：{e}')
                break

        # 保存结果
        json_path = osp.join(self.save_dir_json, f'{user_name}_{self.uid}', 'primary', 'full.json')
        print(f'write json to {json_path}')
        mkdir_if_missing(osp.dirname(json_path))
        write_json(data_list, json_path)
        print(f'dump json file done. total {len(data_list)} urls.\n')

if __name__ == '__main__':
    uid = "182931491"
    save_dir = 'json'
    t = 2
    chrome_path = r'D:\chrome\depot_tools\chromium\src\out\Default\chrome.exe'
    chromedriver_path = r'D:\chrome\depot_tools\chromium\src\out\Default\chromedriver.exe'
    spider = Bilibili_Spider(
        uid=uid,
        executable_path=chromedriver_path,
        save_dir_json=save_dir,
        t=t,
    )
    try:
        spider.get()
    finally:
        spider.close()